home *** CD-ROM | disk | FTP | other *** search
/ PC Advisor 2010 April / PCA177.iso / ESSENTIALS / Firefox Setup.exe / nonlocalized / chrome / browser.jar / content / browser / places / places.js < prev    next >
Encoding:
JavaScript  |  2009-07-15  |  50.1 KB  |  1,354 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is Mozilla Places Organizer.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2005-2006
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Annie Sullivan <annie.sullivan@gmail.com>
  24.  *   Asaf Romano <mano@mozilla.com>
  25.  *   Ehsan Akhgari <ehsan.akhgari@gmail.com>
  26.  *   Drew Willcoxon <adw@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. var PlacesOrganizer = {
  43.   _places: null,
  44.   _content: null,
  45.  
  46.   _initFolderTree: function() {
  47.     var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
  48.     this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
  49.   },
  50.  
  51.   selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) {
  52.     var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
  53.     this._places.selectItems([itemId]);
  54.     // Forcefully expand all-bookmarks
  55.     if (aQueryName == "AllBookmarks")
  56.       asContainer(this._places.selectedNode).containerOpen = true;
  57.   },
  58.  
  59.   init: function PO_init() {
  60.     this._places = document.getElementById("placesList");
  61.     this._content = document.getElementById("placeContent");
  62.     this._initFolderTree();
  63.  
  64.     var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
  65.     if ("arguments" in window && window.arguments.length > 0)
  66.       leftPaneSelection = window.arguments[0];
  67.  
  68.     this.selectLeftPaneQuery(leftPaneSelection);
  69.     // clear the back-stack
  70.     this._backHistory.splice(0);
  71.     document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
  72.  
  73.     var view = this._content.treeBoxObject.view;
  74.     if (view.rowCount > 0)
  75.       view.selection.select(0);
  76.  
  77.     this._content.focus();
  78.  
  79.     // Set up the search UI.
  80.     PlacesSearchBox.init();
  81.  
  82. //@line 86 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  83.  
  84.     window.addEventListener("AppCommand", this, true);
  85. //@line 109 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  86.  
  87.     // remove the "Properties" context-menu item, we've our own details pane
  88.     document.getElementById("placesContext")
  89.             .removeChild(document.getElementById("placesContext_show:info"));
  90.   },
  91.  
  92.   QueryInterface: function PO_QueryInterface(aIID) {
  93.     if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
  94.         aIID.equals(Components.interfaces.nsISupports))
  95.       return this;
  96.  
  97.     throw Components.results.NS_NOINTERFACE;
  98.   },
  99.  
  100.   handleEvent: function PO_handleEvent(aEvent) {
  101.     if (aEvent.type != "AppCommand")
  102.       return;
  103.  
  104.     aEvent.stopPropagation();
  105.     switch (aEvent.command) {
  106.       case "Back":
  107.         if (this._backHistory.length > 0)
  108.           this.back();
  109.         break;
  110.       case "Forward":
  111.         if (this._forwardHistory.length > 0)
  112.           this.forward();
  113.         break;
  114.       case "Search":
  115.         PlacesSearchBox.findAll();
  116.         break;
  117.     }
  118.   },
  119.  
  120.   destroy: function PO_destroy() {
  121.   },
  122.  
  123.   _location: null,
  124.   get location() {
  125.     return this._location;
  126.   },
  127.  
  128.   set location(aLocation) {
  129.     if (!aLocation || this._location == aLocation)
  130.       return aLocation;
  131.  
  132.     if (this.location) {
  133.       this._backHistory.unshift(this.location);
  134.       this._forwardHistory.splice(0);
  135.     }
  136.  
  137.     this._location = aLocation;
  138.     this._places.selectPlaceURI(aLocation);
  139.  
  140.     if (!this._places.hasSelection) {
  141.       // If no node was found for the given place: uri, just load it directly
  142.       this._content.place = aLocation;
  143.     }
  144.     this.onContentTreeSelect();
  145.  
  146.     // update navigation commands
  147.     if (this._backHistory.length == 0)
  148.       document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
  149.     else
  150.       document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
  151.     if (this._forwardHistory.length == 0)
  152.       document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
  153.     else
  154.       document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
  155.  
  156.     return aLocation;
  157.   },
  158.  
  159.   _backHistory: [],
  160.   _forwardHistory: [],
  161.  
  162.   back: function PO_back() {
  163.     this._forwardHistory.unshift(this.location);
  164.     var historyEntry = this._backHistory.shift();
  165.     this._location = null;
  166.     this.location = historyEntry;
  167.   },
  168.   forward: function PO_forward() {
  169.     this._backHistory.unshift(this.location);
  170.     var historyEntry = this._forwardHistory.shift();
  171.     this._location = null;
  172.     this.location = historyEntry;
  173.   },
  174.  
  175.   /**
  176.    * Called when a place folder is selected in the left pane.
  177.    * @param   resetSearchBox
  178.    *          true if the search box should also be reset, false otherwise.
  179.    *          The search box should be reset when a new folder in the left
  180.    *          pane is selected; the search scope and text need to be cleared in
  181.    *          preparation for the new folder.  Note that if the user manually
  182.    *          resets the search box, either by clicking its reset button or by
  183.    *          deleting its text, this will be false.
  184.    */
  185.   _cachedLeftPaneSelectedURI: null,
  186.   onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
  187.     // Don't change the right-hand pane contents when there's no selection.
  188.     if (!this._places.hasSelection)
  189.       return;
  190.  
  191.     var node = this._places.selectedNode;
  192.     var queries = asQuery(node).getQueries({});
  193.  
  194.     // Items are only excluded on the left pane.
  195.     var options = node.queryOptions.clone();
  196.     options.excludeItems = false;
  197.     var placeURI = PlacesUtils.history.queriesToQueryString(queries,
  198.                                                             queries.length,
  199.                                                             options);
  200.  
  201.     // If either the place of the content tree in the right pane has changed or
  202.     // the user cleared the search box, update the place, hide the search UI,
  203.     // and update the back/forward buttons by setting location.
  204.     if (this._content.place != placeURI || !resetSearchBox) {
  205.       this._content.place = placeURI;
  206.       PlacesSearchBox.hideSearchUI();
  207.       this.location = node.uri;
  208.     }
  209.  
  210.     // Update the selected folder title where it appears in the UI: the folder
  211.     // scope button, "Find in <current collection>" command, and the search box
  212.     // emptytext.  They must be updated even if the selection hasn't changed --
  213.     // specifically when node's title changes.  In that case a selection event
  214.     // is generated, this method is called, but the selection does not change.
  215.     var folderButton = document.getElementById("scopeBarFolder");
  216.     var folderTitle = node.title || folderButton.getAttribute("emptytitle");
  217.     folderButton.setAttribute("label", folderTitle);
  218.     var cmd = document.getElementById("OrganizerCommand_find:current");
  219.     var label = PlacesUIUtils.getFormattedString("findInPrefix", [folderTitle]);
  220.     cmd.setAttribute("label", label);
  221.     if (PlacesSearchBox.filterCollection == "collection")
  222.       PlacesSearchBox.updateCollectionTitle(folderTitle);
  223.  
  224.     // When we invalidate a container we use suppressSelectionEvent, when it is
  225.     // unset a select event is fired, in many cases the selection did not really
  226.     // change, so we should check for it, and return early in such a case. Note
  227.     // that we cannot return any earlier than this point, because when
  228.     // !resetSearchBox, we need to update location and hide the UI as above,
  229.     // even though the selection has not changed.
  230.     if (node.uri == this._cachedLeftPaneSelectedURI)
  231.       return;
  232.     this._cachedLeftPaneSelectedURI = node.uri;
  233.  
  234.     // At this point, resetSearchBox is true, because the left pane selection
  235.     // has changed; otherwise we would have returned earlier.
  236.  
  237.     PlacesSearchBox.searchFilter.reset();
  238.     this._setSearchScopeForNode(node);
  239.     if (this._places.treeBoxObject.focused)
  240.       this._fillDetailsPane([node]);
  241.   },
  242.  
  243.   /**
  244.    * Sets the search scope based on aNode's properties.
  245.    * @param   aNode
  246.    *          the node to set up scope from
  247.    */
  248.   _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
  249.     var itemId = aNode.itemId;
  250.     if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
  251.         itemId == PlacesUIUtils.leftPaneQueries["History"]) {
  252.       PlacesQueryBuilder.setScope("history");
  253.     }
  254.     // Default to All Bookmarks for all other nodes, per bug 469437.
  255.     else
  256.       PlacesQueryBuilder.setScope("bookmarks");
  257.  
  258.     // Enable or disable the folder scope button.
  259.     var folderButton = document.getElementById("scopeBarFolder");
  260.     folderButton.hidden = !PlacesUtils.nodeIsFolder(aNode) ||
  261.                           itemId == PlacesUIUtils.allBookmarksFolderId;
  262.   },
  263.  
  264.   /**
  265.    * Handle clicks on the tree.
  266.    * Single Left click, right click or modified click do not result in any
  267.    * special action, since they're related to selection.
  268.    * @param   aEvent
  269.    *          The mouse event.
  270.    */
  271.   onTreeClick: function PO_onTreeClick(aEvent) {
  272.     // Only handle clicks on tree children.
  273.     if (aEvent.target.localName != "treechildren")
  274.       return;
  275.  
  276.     var currentView = aEvent.currentTarget;
  277.     var selectedNode = currentView.selectedNode;
  278.     if (selectedNode) {
  279.       var doubleClickOnFlatList = (aEvent.button == 0 && aEvent.detail == 2 &&
  280.                                    aEvent.target.parentNode.flatList);
  281.       var middleClick = (aEvent.button == 1 && aEvent.detail == 1);
  282.  
  283.       if (PlacesUtils.nodeIsURI(selectedNode) &&
  284.           (doubleClickOnFlatList || middleClick)) {
  285.         // Open associated uri in the browser.
  286.         PlacesOrganizer.openSelectedNode(aEvent);
  287.       }
  288.       else if (middleClick &&
  289.                PlacesUtils.nodeIsContainer(selectedNode)) {
  290.         // The command execution function will take care of seeing if the
  291.         // selection is a folder or a different container type, and will
  292.         // load its contents in tabs.
  293.         PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent);
  294.       }
  295.     }
  296.   },
  297.  
  298.   /**
  299.    * Handle focus changes on the trees.
  300.    * When moving focus between panes we should update the details pane contents.
  301.    * @param   aEvent
  302.    *          The mouse event.
  303.    */
  304.   onTreeFocus: function PO_onTreeFocus(aEvent) {
  305.     var currentView = aEvent.currentTarget;
  306.     var selectedNodes = currentView.selectedNode ? [currentView.selectedNode] :
  307.                         this._content.getSelectionNodes();
  308.     this._fillDetailsPane(selectedNodes);
  309.   },
  310.  
  311.   openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
  312.     if (aContainer.itemId != -1)
  313.       this._places.selectItems([aContainer.itemId]);
  314.     else if (PlacesUtils.nodeIsQuery(aContainer))
  315.       this._places.selectPlaceURI(aContainer.uri);
  316.   },
  317.  
  318.   openSelectedNode: function PO_openSelectedNode(aEvent) {
  319.     PlacesUIUtils.openNodeWithEvent(this._content.selectedNode, aEvent);
  320.   },
  321.  
  322.   /**
  323.    * Returns the options associated with the query currently loaded in the
  324.    * main places pane.
  325.    */
  326.   getCurrentOptions: function PO_getCurrentOptions() {
  327.     return asQuery(this._content.getResult().root).queryOptions;
  328.   },
  329.  
  330.   /**
  331.    * Returns the queries associated with the query currently loaded in the
  332.    * main places pane.
  333.    */
  334.   getCurrentQueries: function PO_getCurrentQueries() {
  335.     return asQuery(this._content.getResult().root).getQueries({});
  336.   },
  337.  
  338.   /**
  339.    * Show the migration wizard for importing from a file.
  340.    */
  341.   importBookmarks: function PO_import() {
  342.     // XXX: ifdef it to be non-modal (non-"sheet") on mac (see bug 259039)
  343.     var features = "modal,centerscreen,chrome,resizable=no";
  344.  
  345.     // The migrator window will set this to true when it closes, if the user
  346.     // chose to migrate from a specific file.
  347.     window.fromFile = false;
  348.     openDialog("chrome://browser/content/migration/migration.xul",
  349.                "migration", features, "bookmarks");
  350.     if (window.fromFile)
  351.       this.importFromFile();
  352.   },
  353.  
  354.   /**
  355.    * Open a file-picker and import the selected file into the bookmarks store
  356.    */
  357.   importFromFile: function PO_importFromFile() {
  358.     var fp = Cc["@mozilla.org/filepicker;1"].
  359.              createInstance(Ci.nsIFilePicker);
  360.     fp.init(window, PlacesUIUtils.getString("SelectImport"),
  361.             Ci.nsIFilePicker.modeOpen);
  362.     fp.appendFilters(Ci.nsIFilePicker.filterHTML);
  363.     if (fp.show() != Ci.nsIFilePicker.returnCancel) {
  364.       if (fp.file) {
  365.         var importer = Cc["@mozilla.org/browser/places/import-export-service;1"].
  366.                        getService(Ci.nsIPlacesImportExportService);
  367.         var file = fp.file.QueryInterface(Ci.nsILocalFile);
  368.         importer.importHTMLFromFile(file, false);
  369.       }
  370.     }
  371.   },
  372.  
  373.   /**
  374.    * Allows simple exporting of bookmarks.
  375.    */
  376.   exportBookmarks: function PO_exportBookmarks() {
  377.     var fp = Cc["@mozilla.org/filepicker;1"].
  378.              createInstance(Ci.nsIFilePicker);
  379.     fp.init(window, PlacesUIUtils.getString("EnterExport"),
  380.             Ci.nsIFilePicker.modeSave);
  381.     fp.appendFilters(Ci.nsIFilePicker.filterHTML);
  382.     fp.defaultString = "bookmarks.html";
  383.     if (fp.show() != Ci.nsIFilePicker.returnCancel) {
  384.       var exporter = Cc["@mozilla.org/browser/places/import-export-service;1"].
  385.                      getService(Ci.nsIPlacesImportExportService);
  386.       exporter.exportHTMLToFile(fp.file);
  387.     }
  388.   },
  389.  
  390.   /**
  391.    * Populates the restore menu with the dates of the backups available.
  392.    */
  393.   populateRestoreMenu: function PO_populateRestoreMenu() {
  394.     var restorePopup = document.getElementById("fileRestorePopup");
  395.  
  396.     var dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
  397.                   getService(Ci.nsIScriptableDateFormat);
  398.  
  399.     // remove existing menu items
  400.     // last item is the restoreFromFile item
  401.     while (restorePopup.childNodes.length > 1)
  402.       restorePopup.removeChild(restorePopup.firstChild);
  403.  
  404.     // get list of files
  405.     var localizedFilename = PlacesUtils.getString("bookmarksArchiveFilename");
  406.     var localizedFilenamePrefix = localizedFilename.substr(0, localizedFilename.indexOf("-"));
  407.     var fileList = [];
  408.     var files = this.bookmarksBackupDir.directoryEntries;
  409.     while (files.hasMoreElements()) {
  410.       var f = files.getNext().QueryInterface(Ci.nsIFile);
  411.       var rx = new RegExp("^(bookmarks|" + localizedFilenamePrefix +
  412.                           ")-([0-9]{4}-[0-9]{2}-[0-9]{2})\.json$");
  413.       if (!f.isHidden() && f.leafName.match(rx)) {
  414.         var date = f.leafName.match(rx)[2].replace(/-/g, "/");
  415.         var dateObj = new Date(date);
  416.         fileList.push({date: dateObj, filename: f.leafName});
  417.       }
  418.     }
  419.  
  420.     fileList.sort(function PO_fileList_compare(a, b) {
  421.       return b.date - a.date;
  422.     });
  423.  
  424.     if (fileList.length == 0)
  425.       return;
  426.  
  427.     // populate menu
  428.     for (var i = 0; i < fileList.length; i++) {
  429.       var m = restorePopup.insertBefore
  430.         (document.createElement("menuitem"),
  431.          document.getElementById("restoreFromFile"));
  432.       m.setAttribute("label",
  433.                      dateSvc.FormatDate("",
  434.                                         Ci.nsIScriptableDateFormat.dateFormatLong,
  435.                                         fileList[i].date.getFullYear(),
  436.                                         fileList[i].date.getMonth() + 1,
  437.                                         fileList[i].date.getDate()));
  438.       m.setAttribute("value", fileList[i].filename);
  439.       m.setAttribute("oncommand",
  440.                      "PlacesOrganizer.onRestoreMenuItemClick(this);");
  441.     }
  442.     restorePopup.insertBefore(document.createElement("menuseparator"),
  443.                               document.getElementById("restoreFromFile"));
  444.   },
  445.  
  446.   /**
  447.    * Called when a menuitem is selected from the restore menu.
  448.    */
  449.   onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) {
  450.     var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  451.                  getService(Ci.nsIProperties);
  452.     var bookmarksFile = dirSvc.get("ProfD", Ci.nsIFile);
  453.     bookmarksFile.append("bookmarkbackups");
  454.     bookmarksFile.append(aMenuItem.getAttribute("value"));
  455.     if (!bookmarksFile.exists())
  456.       return;
  457.     this.restoreBookmarksFromFile(bookmarksFile);
  458.   },
  459.  
  460.   /**
  461.    * Called when 'Choose File...' is selected from the restore menu.
  462.    * Prompts for a file and restores bookmarks to those in the file.
  463.    */
  464.   onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
  465.     var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  466.     fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
  467.             Ci.nsIFilePicker.modeOpen);
  468.     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
  469.                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
  470.     fp.appendFilters(Ci.nsIFilePicker.filterAll);
  471.  
  472.     var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  473.                  getService(Ci.nsIProperties);
  474.     var backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
  475.     fp.displayDirectory = backupsDir;
  476.  
  477.     if (fp.show() != Ci.nsIFilePicker.returnCancel)
  478.       this.restoreBookmarksFromFile(fp.file);
  479.   },
  480.  
  481.   /**
  482.    * Restores bookmarks from a JSON file.
  483.    */
  484.   restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFile) {
  485.     // check file extension
  486.     if (!aFile.leafName.match(/\.json$/)) {
  487.       this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
  488.       return;
  489.     }
  490.  
  491.     // confirm ok to delete existing bookmarks
  492.     var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
  493.                   getService(Ci.nsIPromptService);
  494.     if (!prompts.confirm(null,
  495.                          PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
  496.                          PlacesUIUtils.getString("bookmarksRestoreAlert")))
  497.       return;
  498.  
  499.     try {
  500.       PlacesUtils.restoreBookmarksFromJSONFile(aFile);
  501.     }
  502.     catch(ex) {
  503.       this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
  504.     }
  505.   },
  506.  
  507.   _showErrorAlert: function PO__showErrorAlert(aMsg) {
  508.     var brandShortName = document.getElementById("brandStrings").
  509.                                   getString("brandShortName");
  510.  
  511.     Cc["@mozilla.org/embedcomp/prompt-service;1"].
  512.       getService(Ci.nsIPromptService).
  513.       alert(window, brandShortName, aMsg);
  514.   },
  515.  
  516.   /**
  517.    * Backup bookmarks to desktop, auto-generate a filename with a date.
  518.    * The file is a JSON serialization of bookmarks, tags and any annotations
  519.    * of those items.
  520.    */
  521.   backupBookmarks: function PO_backupBookmarks() {
  522.     var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  523.     fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
  524.             Ci.nsIFilePicker.modeSave);
  525.     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
  526.                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
  527.  
  528.     var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  529.                  getService(Ci.nsIProperties);
  530.     var backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
  531.     fp.displayDirectory = backupsDir;
  532.  
  533.     // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
  534.     // and makes the alphabetical order of multiple backup files more useful.
  535.     var date = (new Date).toLocaleFormat("%Y-%m-%d");
  536.     fp.defaultString = PlacesUIUtils.getFormattedString("bookmarksBackupFilenameJSON",
  537.                                                         [date]);
  538.  
  539.     if (fp.show() != Ci.nsIFilePicker.returnCancel) {
  540.       PlacesUtils.backupBookmarksToFile(fp.file);
  541.  
  542.       // copy new backup to /backups dir (bug 424389)
  543.       var latestBackup = PlacesUtils.getMostRecentBackup();
  544.       if (!latestBackup || latestBackup != fp.file) {
  545.         latestBackup.remove(false);
  546.         var date = new Date().toLocaleFormat("%Y-%m-%d");
  547.         var name = PlacesUtils.getFormattedString("bookmarksArchiveFilename",
  548.                                                   [date]);
  549.         fp.file.copyTo(this.bookmarksBackupDir, name);
  550.       }
  551.     }
  552.   },
  553.  
  554.   get bookmarksBackupDir() {
  555.     delete this.bookmarksBackupDir;
  556.     var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  557.                  getService(Ci.nsIProperties);
  558.     var bookmarksBackupDir = dirSvc.get("ProfD", Ci.nsIFile);
  559.     bookmarksBackupDir.append("bookmarkbackups");
  560.     if (!bookmarksBackupDir.exists())
  561.       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
  562.     return this.bookmarksBackupDir = bookmarksBackupDir;
  563.   },
  564.  
  565.   _paneDisabled: false,
  566.   _setDetailsFieldsDisabledState:
  567.   function PO__setDetailsFieldsDisabledState(aDisabled) {
  568.     if (aDisabled) {
  569.       document.getElementById("paneElementsBroadcaster")
  570.               .setAttribute("disabled", "true");
  571.     }
  572.     else {
  573.       document.getElementById("paneElementsBroadcaster")
  574.               .removeAttribute("disabled");
  575.     }
  576.   },
  577.  
  578.   _detectAndSetDetailsPaneMinimalState:
  579.   function PO__detectAndSetDetailsPaneMinimalState(aNode) {
  580.     /**
  581.      * The details of simple folder-items (as opposed to livemarks) or the
  582.      * of livemark-children are not likely to fill the infoBox anyway,
  583.      * thus we remove the "More/Less" button and show all details.
  584.      *
  585.      * the wasminimal attribute here is used to persist the "more/less"
  586.      * state in a bookmark->folder->bookmark scenario.
  587.      */
  588.     var infoBox = document.getElementById("infoBox");
  589.     var infoBoxExpander = document.getElementById("infoBoxExpander");
  590.     var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
  591.  
  592.     if (!aNode) {
  593.       infoBoxExpanderWrapper.hidden = true;
  594.       return;
  595.     }
  596.     if (aNode.itemId != -1 &&
  597.         ((PlacesUtils.nodeIsFolder(aNode) &&
  598.           !PlacesUtils.nodeIsLivemarkContainer(aNode)) ||
  599.          PlacesUtils.nodeIsLivemarkItem(aNode))) {
  600.       if (infoBox.getAttribute("minimal") == "true")
  601.         infoBox.setAttribute("wasminimal", "true");
  602.       infoBox.removeAttribute("minimal");
  603.       infoBoxExpanderWrapper.hidden = true;
  604.     }
  605.     else {
  606.       if (infoBox.getAttribute("wasminimal") == "true")
  607.         infoBox.setAttribute("minimal", "true");
  608.       infoBox.removeAttribute("wasminimal");
  609.       infoBoxExpanderWrapper.hidden = false;
  610.     }
  611.   },
  612.  
  613.   // NOT YET USED
  614.   updateThumbnailProportions: function PO_updateThumbnailProportions() {
  615.     var previewBox = document.getElementById("previewBox");
  616.     var canvas = document.getElementById("itemThumbnail");
  617.     var height = previewBox.boxObject.height;
  618.     var width = height * (screen.width / screen.height);
  619.     canvas.width = width;
  620.     canvas.height = height;
  621.   },
  622.  
  623.   onContentTreeSelect: function PO_onContentTreeSelect() {
  624.     if (this._content.treeBoxObject.focused)
  625.       this._fillDetailsPane(this._content.getSelectionNodes());
  626.   },
  627.  
  628.   _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
  629.     var infoBox = document.getElementById("infoBox");
  630.     var detailsDeck = document.getElementById("detailsDeck");
  631.  
  632.     // Make sure the infoBox UI is visible if we need to use it, we hide it
  633.     // below when we don't.
  634.     infoBox.hidden = false;
  635.     var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
  636.     // If a textbox within a panel is focused, force-blur it so its contents
  637.     // are saved
  638.     if (gEditItemOverlay.itemId != -1) {
  639.       var focusedElement = document.commandDispatcher.focusedElement;
  640.       if ((focusedElement instanceof HTMLInputElement ||
  641.            focusedElement instanceof HTMLTextAreaElement) &&
  642.           /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
  643.         focusedElement.blur();
  644.  
  645.       // don't update the panel if we are already editing this node unless we're
  646.       // in multi-edit mode
  647.       if (aSelectedNode && gEditItemOverlay.itemId == aSelectedNode.itemId &&
  648.           detailsDeck.selectedIndex == 1 && !gEditItemOverlay.multiEdit)
  649.         return;
  650.     }
  651.  
  652.     // Clean up the panel before initing it again.
  653.     gEditItemOverlay.uninitPanel(false);
  654.  
  655.     if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
  656.       detailsDeck.selectedIndex = 1;
  657.       // Using the concrete itemId is arguably wrong.  The bookmarks API
  658.       // does allow setting properties for folder shortcuts as well, but since
  659.       // the UI does not distinct between the couple, we better just show
  660.       // the concrete item properties for shortcuts to root nodes.
  661.       var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
  662.       var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
  663.       var readOnly = isRootItem ||
  664.                      aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
  665.       var useConcreteId = isRootItem ||
  666.                           PlacesUtils.nodeIsTagQuery(aSelectedNode);
  667.       var itemId = -1;
  668.       if (concreteId != -1 && useConcreteId)
  669.         itemId = concreteId;
  670.       else if (aSelectedNode.itemId != -1)
  671.         itemId = aSelectedNode.itemId;
  672.       else
  673.         itemId = PlacesUtils._uri(aSelectedNode.uri);
  674.  
  675.       gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"],
  676.                                            forceReadOnly: readOnly });
  677.  
  678.       // Dynamically generated queries, like history date containers, have
  679.       // itemId !=0 and do not exist in history.  For them the panel is
  680.       // read-only, but empty, since it can't get a valid title for the object.
  681.       // In such a case we force the title using the selectedNode one, for UI
  682.       // polishness.
  683.       if (aSelectedNode.itemId == -1 &&
  684.           (PlacesUtils.nodeIsDay(aSelectedNode) ||
  685.            PlacesUtils.nodeIsHost(aSelectedNode)))
  686.         gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
  687.  
  688.       this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
  689.     }
  690.     else if (!aSelectedNode && aNodeList[0]) {
  691.       var itemIds = [];
  692.       for (var i = 0; i < aNodeList.length; i++) {
  693.         if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
  694.             !PlacesUtils.nodeIsURI(aNodeList[i])) {
  695.           detailsDeck.selectedIndex = 0;
  696.           var selectItemDesc = document.getElementById("selectItemDescription");
  697.           var itemsCountLabel = document.getElementById("itemsCountText");
  698.           selectItemDesc.hidden = false;
  699.           itemsCountLabel.value =
  700.             PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
  701.                                              [aNodeList.length]);
  702.           infoBox.hidden = true;
  703.           return;
  704.         }
  705.         itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
  706.                      PlacesUtils._uri(aNodeList[i].uri);
  707.       }
  708.       detailsDeck.selectedIndex = 1;
  709.       gEditItemOverlay.initPanel(itemIds,
  710.                                  { hiddenRows: ["folderPicker",
  711.                                                 "loadInSidebar",
  712.                                                 "location",
  713.                                                 "keyword",
  714.                                                 "description",
  715.                                                 "name"]});
  716.       this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
  717.     }
  718.     else {
  719.       detailsDeck.selectedIndex = 0;
  720.       infoBox.hidden = true;
  721.       var selectItemDesc = document.getElementById("selectItemDescription");
  722.       var itemsCountLabel = document.getElementById("itemsCountText");
  723.       var rowCount = this._content.treeBoxObject.view.rowCount;
  724.       if (rowCount == 0) {
  725.         selectItemDesc.hidden = true;
  726.         itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
  727.       }
  728.       else {
  729.         selectItemDesc.hidden = false;
  730.         if (rowCount == 1)
  731.           itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.oneItem");
  732.         else {
  733.           itemsCountLabel.value =
  734.             PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
  735.                                              [rowCount]);
  736.         }
  737.       }
  738.     }
  739.   },
  740.  
  741.   // NOT YET USED
  742.   _updateThumbnail: function PO__updateThumbnail() {
  743.     var bo = document.getElementById("previewBox").boxObject;
  744.     var width  = bo.width;
  745.     var height = bo.height;
  746.  
  747.     var canvas = document.getElementById("itemThumbnail");
  748.     var ctx = canvas.getContext('2d');
  749.     var notAvailableText = canvas.getAttribute("notavailabletext");
  750.     ctx.save();
  751.     ctx.fillStyle = "-moz-Dialog";
  752.     ctx.fillRect(0, 0, width, height);
  753.     ctx.translate(width/2, height/2);
  754.  
  755.     ctx.fillStyle = "GrayText";
  756.     ctx.mozTextStyle = "12pt sans serif";
  757.     var len = ctx.mozMeasureText(notAvailableText);
  758.     ctx.translate(-len/2,0);
  759.     ctx.mozDrawText(notAvailableText);
  760.     ctx.restore();
  761.   },
  762.  
  763.   toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
  764.     var infoBox = document.getElementById("infoBox");
  765.     var infoBoxExpander = document.getElementById("infoBoxExpander");
  766.     var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
  767.  
  768.     if (infoBox.getAttribute("minimal") == "true") {
  769.       infoBox.removeAttribute("minimal");
  770.       infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
  771.       infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
  772.       infoBoxExpander.className = "expander-up";
  773.     }
  774.     else {
  775.       infoBox.setAttribute("minimal", "true");
  776.       infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
  777.       infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
  778.       infoBoxExpander.className = "expander-down";
  779.     }
  780.   },
  781.  
  782.   /**
  783.    * Save the current search (or advanced query) to the bookmarks root.
  784.    */
  785.   saveSearch: function PO_saveSearch() {
  786.     // Get the place: uri for the query.
  787.     // If the advanced query builder is showing, use that.
  788.     var options = this.getCurrentOptions();
  789.  
  790. //@line 816 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  791.     var queries = this.getCurrentQueries();
  792. //@line 818 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  793.  
  794.     var placeSpec = PlacesUtils.history.queriesToQueryString(queries,
  795.                                                              queries.length,
  796.                                                              options);
  797.     var placeURI = Cc["@mozilla.org/network/io-service;1"].
  798.                    getService(Ci.nsIIOService).
  799.                    newURI(placeSpec, null, null);
  800.  
  801.     // Prompt the user for a name for the query.
  802.     // XXX - using prompt service for now; will need to make
  803.     // a real dialog and localize when we're sure this is the UI we want.
  804.     var title = PlacesUIUtils.getString("saveSearch.title");
  805.     var inputLabel = PlacesUIUtils.getString("saveSearch.inputLabel");
  806.     var defaultText = PlacesUIUtils.getString("saveSearch.inputDefaultText");
  807.  
  808.     var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
  809.                   getService(Ci.nsIPromptService);
  810.     var check = {value: false};
  811.     var input = {value: defaultText};
  812.     var save = prompts.prompt(null, title, inputLabel, input, null, check);
  813.  
  814.     // Don't add the query if the user cancels or clears the seach name.
  815.     if (!save || input.value == "")
  816.      return;
  817.  
  818.     // Add the place: uri as a bookmark under the bookmarks root.
  819.     var txn = PlacesUIUtils.ptm.createItem(placeURI,
  820.                                            PlacesUtils.bookmarksMenuFolderId,
  821.                                            PlacesUtils.bookmarks.DEFAULT_INDEX,
  822.                                            input.value);
  823.     PlacesUIUtils.ptm.doTransaction(txn);
  824.  
  825.     // select and load the new query
  826.     this._places.selectPlaceURI(placeSpec);
  827.   }
  828. };
  829.  
  830. /**
  831.  * A set of utilities relating to search within Bookmarks and History.
  832.  */
  833. var PlacesSearchBox = {
  834.  
  835.   /**
  836.    * The Search text field
  837.    */
  838.   get searchFilter() {
  839.     return document.getElementById("searchFilter");
  840.   },
  841.  
  842.   /**
  843.    * Folders to include when searching.
  844.    */
  845.   _folders: [],
  846.   get folders() {
  847.     if (this._folders.length == 0)
  848.       this._folders.push(PlacesUtils.bookmarksMenuFolderId,
  849.                          PlacesUtils.unfiledBookmarksFolderId,
  850.                          PlacesUtils.toolbarFolderId);
  851.     return this._folders;
  852.   },
  853.   set folders(aFolders) {
  854.     this._folders = aFolders;
  855.     return aFolders;
  856.   },
  857.  
  858.   /**
  859.    * Run a search for the specified text, over the collection specified by
  860.    * the dropdown arrow. The default is all bookmarks, but can be
  861.    * localized to the active collection.
  862.    * @param   filterString
  863.    *          The text to search for.
  864.    */
  865.   search: function PSB_search(filterString) {
  866.     var PO = PlacesOrganizer;
  867.     // If the user empties the search box manually, reset it and load all
  868.     // contents of the current scope.
  869.     // XXX this might be to jumpy, maybe should search for "", so results
  870.     // are ungrouped, and search box not reset
  871.     if (filterString == "") {
  872.       PO.onPlaceSelected(false);
  873.       return;
  874.     }
  875.  
  876.     var currentOptions = PO.getCurrentOptions();
  877.     var content = PO._content;
  878.  
  879.     // Search according to the current scope and folders, which were set by
  880.     // PQB_setScope()
  881.     switch (PlacesSearchBox.filterCollection) {
  882.     case "collection":
  883.       content.applyFilter(filterString, this.folders);
  884.       // XXX changing the button text is badness
  885.       //var scopeBtn = document.getElementById("scopeBarFolder");
  886.       //scopeBtn.label = PlacesOrganizer._places.selectedNode.title;
  887.       break;
  888.     case "bookmarks":
  889.       content.applyFilter(filterString, this.folders);
  890.       break;
  891.     case "history":
  892.       if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
  893.         var query = PlacesUtils.history.getNewQuery();
  894.         query.searchTerms = filterString;
  895.         var options = currentOptions.clone();
  896.         // Make sure we're getting uri results.
  897.         options.resultType = currentOptions.RESULT_TYPE_URI;
  898.         options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
  899.         content.load([query], options);
  900.       }
  901.       else
  902.         content.applyFilter(filterString);
  903.       break;
  904.     default:
  905.       throw "Invalid filterCollection on search";
  906.       break;
  907.     }
  908.  
  909.     PlacesSearchBox.showSearchUI();
  910.  
  911.     // Update the details panel
  912.     PlacesOrganizer.onContentTreeSelect();
  913.   },
  914.  
  915.   /**
  916.    * Finds across all bookmarks
  917.    */
  918.   findAll: function PSB_findAll() {
  919.     PlacesQueryBuilder.setScope("bookmarks");
  920.     this.focus();
  921.   },
  922.  
  923.   /**
  924.    * Finds in the currently selected Place.
  925.    */
  926.   findCurrent: function PSB_findCurrent() {
  927.     PlacesQueryBuilder.setScope("collection");
  928.     this.focus();
  929.   },
  930.  
  931.   /**
  932.    * Updates the display with the title of the current collection.
  933.    * @param   title
  934.    *          The title of the current collection.
  935.    */
  936.   updateCollectionTitle: function PSB_updateCollectionTitle(title) {
  937.     if (title)
  938.       this.searchFilter.emptyText =
  939.         PlacesUIUtils.getFormattedString("searchCurrentDefault", [title]);
  940.     else
  941.       this.searchFilter.emptyText = this.filterCollection == "history" ?
  942.                                     PlacesUIUtils.getString("searchHistory") :
  943.                                     PlacesUIUtils.getString("searchBookmarks");
  944.   },
  945.  
  946.   /**
  947.    * Gets/sets the active collection from the dropdown menu.
  948.    */
  949.   get filterCollection() {
  950.     return this.searchFilter.getAttribute("collection");
  951.   },
  952.   set filterCollection(collectionName) {
  953.     if (collectionName == this.filterCollection)
  954.       return collectionName;
  955.  
  956.     this.searchFilter.setAttribute("collection", collectionName);
  957.  
  958.     var newGrayText = null;
  959.     if (collectionName == "collection") {
  960.       newGrayText = PlacesOrganizer._places.selectedNode.title ||
  961.                     document.getElementById("scopeBarFolder").
  962.                       getAttribute("emptytitle");
  963.     }
  964.     this.updateCollectionTitle(newGrayText);
  965.     return collectionName;
  966.   },
  967.  
  968.   /**
  969.    * Focus the search box
  970.    */
  971.   focus: function PSB_focus() {
  972.     this.searchFilter.focus();
  973.   },
  974.  
  975.   /**
  976.    * Set up the gray text in the search bar as the Places View loads.
  977.    */
  978.   init: function PSB_init() {
  979.     this.updateCollectionTitle();
  980.   },
  981.  
  982.   /**
  983.    * Gets or sets the text shown in the Places Search Box
  984.    */
  985.   get value() {
  986.     return this.searchFilter.value;
  987.   },
  988.   set value(value) {
  989.     return this.searchFilter.value = value;
  990.   },
  991.  
  992.   showSearchUI: function PSB_showSearchUI() {
  993.     // Hide the advanced search controls when the user hasn't searched
  994.     var searchModifiers = document.getElementById("searchModifiers");
  995.     searchModifiers.hidden = false;
  996.  
  997. //@line 1027 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  998.   },
  999.  
  1000.   hideSearchUI: function PSB_hideSearchUI() {
  1001.     var searchModifiers = document.getElementById("searchModifiers");
  1002.     searchModifiers.hidden = true;
  1003.   }
  1004. };
  1005.  
  1006. /**
  1007.  * Functions and data for advanced query builder
  1008.  */
  1009. var PlacesQueryBuilder = {
  1010.  
  1011.   queries: [],
  1012.   queryOptions: null,
  1013.  
  1014. //@line 1506 "e:\builds\moz2_slave\win32_build\build\browser\components\places\content\places.js"
  1015.  
  1016.   /**
  1017.    * Called when a scope button in the scope bar is clicked.
  1018.    * @param   aButton
  1019.    *          the scope button that was selected
  1020.    */
  1021.   onScopeSelected: function PQB_onScopeSelected(aButton) {
  1022.     switch (aButton.id) {
  1023.     case "scopeBarHistory":
  1024.       this.setScope("history");
  1025.       break;
  1026.     case "scopeBarFolder":
  1027.       this.setScope("collection");
  1028.       break;
  1029.     case "scopeBarAll":
  1030.       this.setScope("bookmarks");
  1031.       break;
  1032.     default:
  1033.       throw "Invalid search scope button ID";
  1034.       break;
  1035.     }
  1036.   },
  1037.  
  1038.   /**
  1039.    * Sets the search scope.  This can be called when no search is active, and
  1040.    * in that case, when the user does begin a search aScope will be used (see
  1041.    * PSB_search()).  If there is an active search, it's performed again to
  1042.    * update the content tree.
  1043.    * @param   aScope
  1044.    *          the search scope, "bookmarks", "collection", or "history"
  1045.    */
  1046.   setScope: function PQB_setScope(aScope) {
  1047.     // Determine filterCollection, folders, and scopeButtonId based on aScope.
  1048.     var filterCollection;
  1049.     var folders = [];
  1050.     var scopeButtonId;
  1051.     switch (aScope) {
  1052.     case "history":
  1053.       filterCollection = "history";
  1054.       scopeButtonId = "scopeBarHistory";
  1055.       break;
  1056.     case "collection":
  1057.       // The folder scope button can only become hidden upon selecting a new
  1058.       // folder in the left pane, and the disabled state will remain unchanged
  1059.       // until a new folder is selected.  See PO__setScopeForNode().
  1060.       if (!document.getElementById("scopeBarFolder").hidden) {
  1061.         filterCollection = "collection";
  1062.         scopeButtonId = "scopeBarFolder";
  1063.         folders.push(PlacesUtils.getConcreteItemId(
  1064.                        PlacesOrganizer._places.selectedNode));
  1065.         break;
  1066.       }
  1067.       // Fall through.  If collection scope doesn't make sense for the
  1068.       // selected node, choose bookmarks scope.
  1069.     case "bookmarks":
  1070.       filterCollection = "bookmarks";
  1071.       scopeButtonId = "scopeBarAll";
  1072.       folders.push(PlacesUtils.bookmarksMenuFolderId,
  1073.                    PlacesUtils.toolbarFolderId,
  1074.                    PlacesUtils.unfiledBookmarksFolderId);
  1075.       break;
  1076.     default:
  1077.       throw "Invalid search scope";
  1078.       break;
  1079.     }
  1080.  
  1081.     // Check the appropriate scope button in the scope bar.
  1082.     document.getElementById(scopeButtonId).checked = true;
  1083.  
  1084.     // Update the search box.  Re-search if there's an active search.
  1085.     PlacesSearchBox.filterCollection = filterCollection;
  1086.     PlacesSearchBox.folders = folders;
  1087.     var searchStr = PlacesSearchBox.searchFilter.value;
  1088.     if (searchStr)
  1089.       PlacesSearchBox.search(searchStr);
  1090.   }
  1091. };
  1092.  
  1093. /**
  1094.  * Population and commands for the View Menu.
  1095.  */
  1096. var ViewMenu = {
  1097.   /**
  1098.    * Removes content generated previously from a menupopup.
  1099.    * @param   popup
  1100.    *          The popup that contains the previously generated content.
  1101.    * @param   startID
  1102.    *          The id attribute of an element that is the start of the
  1103.    *          dynamically generated region - remove elements after this
  1104.    *          item only.
  1105.    *          Must be contained by popup. Can be null (in which case the
  1106.    *          contents of popup are removed).
  1107.    * @param   endID
  1108.    *          The id attribute of an element that is the end of the
  1109.    *          dynamically generated region - remove elements up to this
  1110.    *          item only.
  1111.    *          Must be contained by popup. Can be null (in which case all
  1112.    *          items until the end of the popup will be removed). Ignored
  1113.    *          if startID is null.
  1114.    * @returns The element for the caller to insert new items before,
  1115.    *          null if the caller should just append to the popup.
  1116.    */
  1117.   _clean: function VM__clean(popup, startID, endID) {
  1118.     if (endID)
  1119.       NS_ASSERT(startID, "meaningless to have valid endID and null startID");
  1120.     if (startID) {
  1121.       var startElement = document.getElementById(startID);
  1122.       NS_ASSERT(startElement.parentNode ==
  1123.                 popup, "startElement is not in popup");
  1124.       NS_ASSERT(startElement,
  1125.                 "startID does not correspond to an existing element");
  1126.       var endElement = null;
  1127.       if (endID) {
  1128.         endElement = document.getElementById(endID);
  1129.         NS_ASSERT(endElement.parentNode == popup,
  1130.                   "endElement is not in popup");
  1131.         NS_ASSERT(endElement,
  1132.                   "endID does not correspond to an existing element");
  1133.       }
  1134.       while (startElement.nextSibling != endElement)
  1135.         popup.removeChild(startElement.nextSibling);
  1136.       return endElement;
  1137.     }
  1138.     else {
  1139.       while(popup.hasChildNodes())
  1140.         popup.removeChild(popup.firstChild);
  1141.     }
  1142.     return null;
  1143.   },
  1144.  
  1145.   /**
  1146.    * Fills a menupopup with a list of columns
  1147.    * @param   event
  1148.    *          The popupshowing event that invoked this function.
  1149.    * @param   startID
  1150.    *          see _clean
  1151.    * @param   endID
  1152.    *          see _clean
  1153.    * @param   type
  1154.    *          the type of the menuitem, e.g. "radio" or "checkbox".
  1155.    *          Can be null (no-type).
  1156.    *          Checkboxes are checked if the column is visible.
  1157.    * @param   propertyPrefix
  1158.    *          If propertyPrefix is non-null:
  1159.    *          propertyPrefix + column ID + ".label" will be used to get the
  1160.    *          localized label string.
  1161.    *          propertyPrefix + column ID + ".accesskey" will be used to get the
  1162.    *          localized accesskey.
  1163.    *          If propertyPrefix is null, the column label is used as label and
  1164.    *          no accesskey is assigned.
  1165.    */
  1166.   fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
  1167.     var popup = event.target;
  1168.     var pivot = this._clean(popup, startID, endID);
  1169.  
  1170.     // If no column is "sort-active", the "Unsorted" item needs to be checked,
  1171.     // so track whether or not we find a column that is sort-active.
  1172.     var isSorted = false;
  1173.     var content = document.getElementById("placeContent");
  1174.     var columns = content.columns;
  1175.     for (var i = 0; i < columns.count; ++i) {
  1176.       var column = columns.getColumnAt(i).element;
  1177.       var menuitem = document.createElement("menuitem");
  1178.       menuitem.id = "menucol_" + column.id;
  1179.       menuitem.column = column;
  1180.       var label = column.getAttribute("label");
  1181.       if (propertyPrefix) {
  1182.         var menuitemPrefix = propertyPrefix;
  1183.         // for string properties, use "name" as the id, instead of "title"
  1184.         // see bug #386287 for details
  1185.         var columnId = column.getAttribute("anonid");
  1186.         menuitemPrefix += columnId == "title" ? "name" : columnId;
  1187.         label = PlacesUIUtils.getString(menuitemPrefix + ".label");
  1188.         var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
  1189.         menuitem.setAttribute("accesskey", accesskey);
  1190.       }
  1191.       menuitem.setAttribute("label", label);
  1192.       if (type == "radio") {
  1193.         menuitem.setAttribute("type", "radio");
  1194.         menuitem.setAttribute("name", "columns");
  1195.         // This column is the sort key. Its item is checked.
  1196.         if (column.getAttribute("sortDirection") != "") {
  1197.           menuitem.setAttribute("checked", "true");
  1198.           isSorted = true;
  1199.         }
  1200.       }
  1201.       else if (type == "checkbox") {
  1202.         menuitem.setAttribute("type", "checkbox");
  1203.         // Cannot uncheck the primary column.
  1204.         if (column.getAttribute("primary") == "true")
  1205.           menuitem.setAttribute("disabled", "true");
  1206.         // Items for visible columns are checked.
  1207.         if (!column.hidden)
  1208.           menuitem.setAttribute("checked", "true");
  1209.       }
  1210.       if (pivot)
  1211.         popup.insertBefore(menuitem, pivot);
  1212.       else
  1213.         popup.appendChild(menuitem);
  1214.     }
  1215.     event.stopPropagation();
  1216.   },
  1217.  
  1218.   /**
  1219.    * Set up the content of the view menu.
  1220.    */
  1221.   populateSortMenu: function VM_populateSortMenu(event) {
  1222.     this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.");
  1223.  
  1224.     var sortColumn = this._getSortColumn();
  1225.     var viewSortAscending = document.getElementById("viewSortAscending");
  1226.     var viewSortDescending = document.getElementById("viewSortDescending");
  1227.     // We need to remove an existing checked attribute because the unsorted
  1228.     // menu item is not rebuilt every time we open the menu like the others.
  1229.     var viewUnsorted = document.getElementById("viewUnsorted");
  1230.     if (!sortColumn) {
  1231.       viewSortAscending.removeAttribute("checked");
  1232.       viewSortDescending.removeAttribute("checked");
  1233.       viewUnsorted.setAttribute("checked", "true");
  1234.     }
  1235.     else if (sortColumn.getAttribute("sortDirection") == "ascending") {
  1236.       viewSortAscending.setAttribute("checked", "true");
  1237.       viewSortDescending.removeAttribute("checked");
  1238.       viewUnsorted.removeAttribute("checked");
  1239.     }
  1240.     else if (sortColumn.getAttribute("sortDirection") == "descending") {
  1241.       viewSortDescending.setAttribute("checked", "true");
  1242.       viewSortAscending.removeAttribute("checked");
  1243.       viewUnsorted.removeAttribute("checked");
  1244.     }
  1245.   },
  1246.  
  1247.   /**
  1248.    * Shows/Hides a tree column.
  1249.    * @param   element
  1250.    *          The menuitem element for the column
  1251.    */
  1252.   showHideColumn: function VM_showHideColumn(element) {
  1253.     var column = element.column;
  1254.  
  1255.     var splitter = column.nextSibling;
  1256.     if (splitter && splitter.localName != "splitter")
  1257.       splitter = null;
  1258.  
  1259.     if (element.getAttribute("checked") == "true") {
  1260.       column.setAttribute("hidden", "false");
  1261.       if (splitter)
  1262.         splitter.removeAttribute("hidden");
  1263.     }
  1264.     else {
  1265.       column.setAttribute("hidden", "true");
  1266.       if (splitter)
  1267.         splitter.setAttribute("hidden", "true");
  1268.     }
  1269.   },
  1270.  
  1271.   /**
  1272.    * Gets the last column that was sorted.
  1273.    * @returns  the currently sorted column, null if there is no sorted column.
  1274.    */
  1275.   _getSortColumn: function VM__getSortColumn() {
  1276.     var content = document.getElementById("placeContent");
  1277.     var cols = content.columns;
  1278.     for (var i = 0; i < cols.count; ++i) {
  1279.       var column = cols.getColumnAt(i).element;
  1280.       var sortDirection = column.getAttribute("sortDirection");
  1281.       if (sortDirection == "ascending" || sortDirection == "descending")
  1282.         return column;
  1283.     }
  1284.     return null;
  1285.   },
  1286.  
  1287.   /**
  1288.    * Sorts the view by the specified column.
  1289.    * @param   aColumn
  1290.    *          The colum that is the sort key. Can be null - the
  1291.    *          current sort column or the title column will be used.
  1292.    * @param   aDirection
  1293.    *          The direction to sort - "ascending" or "descending".
  1294.    *          Can be null - the last direction or descending will be used.
  1295.    *
  1296.    * If both aColumnID and aDirection are null, the view will be unsorted.
  1297.    */
  1298.   setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
  1299.     var result = document.getElementById("placeContent").getResult();
  1300.     if (!aColumn && !aDirection) {
  1301.       result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1302.       return;
  1303.     }
  1304.  
  1305.     var columnId;
  1306.     if (aColumn) {
  1307.       columnId = aColumn.getAttribute("anonid");
  1308.       if (!aDirection) {
  1309.         var sortColumn = this._getSortColumn();
  1310.         if (sortColumn)
  1311.           aDirection = sortColumn.getAttribute("sortDirection");
  1312.       }
  1313.     }
  1314.     else {
  1315.       var sortColumn = this._getSortColumn();
  1316.       columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
  1317.     }
  1318.  
  1319.     // This maps the possible values of columnId (i.e., anonid's of treecols in
  1320.     // placeContent) to the default sortingMode and sortingAnnotation values for
  1321.     // each column.
  1322.     //   key:  Sort key in the name of one of the
  1323.     //         nsINavHistoryQueryOptions.SORT_BY_* constants
  1324.     //   dir:  Default sort direction to use if none has been specified
  1325.     //   anno: The annotation to sort by, if key is "ANNOTATION"
  1326.     var colLookupTable = {
  1327.       title:        { key: "TITLE",        dir: "ascending"  },
  1328.       tags:         { key: "TAGS",         dir: "ascending"  },
  1329.       url:          { key: "URI",          dir: "ascending"  },
  1330.       date:         { key: "DATE",         dir: "descending" },
  1331.       visitCount:   { key: "VISITCOUNT",   dir: "descending" },
  1332.       keyword:      { key: "KEYWORD",      dir: "ascending"  },
  1333.       dateAdded:    { key: "DATEADDED",    dir: "descending" },
  1334.       lastModified: { key: "LASTMODIFIED", dir: "descending" },
  1335.       description:  { key: "ANNOTATION",
  1336.                       dir: "ascending",
  1337.                       anno: DESCRIPTION_ANNO }
  1338.     };
  1339.  
  1340.     // Make sure we have a valid column.
  1341.     if (!colLookupTable.hasOwnProperty(columnId))
  1342.       throw("Invalid column");
  1343.  
  1344.     // Use a default sort direction if none has been specified.  If aDirection
  1345.     // is invalid, result.sortingMode will be undefined, which has the effect
  1346.     // of unsorting the tree.
  1347.     aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
  1348.  
  1349.     var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
  1350.     result.sortingAnnotation = colLookupTable[columnId].anno || "";
  1351.     result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
  1352.   }
  1353. };
  1354.